using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Text;

using Pegasus.Diagnostics;

namespace Pegasus.Reflection
{
	/// <summary>
	/// The type name takes a fully qualified name in the form of "type, assembly, version, culture, pulbic key", etc... and
	/// break it up into it's compments for easy changes.
	/// </summary>
	public class TypeName
	{
		// TODO: All the parsing could be done in RegEx if someone knows it will enought.

		// Local Instance Values
		private string m_displayName = null;
		private string m_baseTypeName = string.Empty;
		private string m_fullTypeName = string.Empty;
		private bool m_isArray = false;

		private string m_fullAssemblyName = string.Empty;
		private string m_assemblyName = string.Empty;

		private Version m_version = EmptyVersion;
		private CultureInfo m_culture = InvariantCulture; 
		private string m_publicKey = null;

		private bool m_isGenericType = false;
        private bool m_isArrayItemGenericType = false;
		private TypeName[] m_genericParameterTypeNames = new TypeName[ 0 ];

		// Local Static Values
		private static readonly Version EmptyVersion = new Version( 0, 0, 0, 0 );
		private static readonly CultureInfo InvariantCulture = new CultureInfo( 0x007F );

		// Local Const Values
		private const char TypeAssemblySeperator = ',';
		private const char SubTypeSeperator = '+';

		private const char NameValueSeperator = '=';

		private const char ArrayLeader = '[';
		private const char ArrayTrailer = ']';

		private const char GenericParameterStart = '`';
		private const char GenericParameterLeader = '[';
		private const char GenericParameterTrailer = ']';
		private const char GenericParameterSeperator = ',';

		/// <summary>
		/// Initializes a new instance of the <see cref="T:TypeName"/> class.
		/// </summary>
		/// <param name="assemblyQualifiedName">Type of the fully qualified name of the type and assembly.</param>
		public TypeName( string assemblyQualifiedName )
		{
			// Check Parameter
			ParamCode.AssertNotEmpty( assemblyQualifiedName, "assemblyQualifiedName" );

			ParseQualifiedName( assemblyQualifiedName, out m_baseTypeName, out m_fullTypeName, out m_fullAssemblyName, out m_isArray, out m_genericParameterTypeNames );
			FinishSetup();
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="T:TypeName"/> class.
		/// </summary>
		/// <param name="assemblyQualifiedName">Name of the assembly qualified.</param>
		/// <param name="typeName">Name of the type.</param>
		public TypeName( string assemblyQualifiedName, string typeName ) 
			: this( string.Format( "{0}, {1}", typeName, assemblyQualifiedName ) )
		{
			// Check Parameters
			ParamCode.AssertNotEmpty( assemblyQualifiedName, "assemblyQualifiedName" );
			ParamCode.AssertNotEmpty( typeName, "typeName" );
		}

		/// <summary>
		/// Gets a value indicating whether this instance is generic type.
		/// </summary>
		/// <value>
		/// 	<c>true</c> if this instance is generic type; otherwise, <c>false</c>.
		/// </value>
		public bool IsGenericType
		{
			get
			{
				return m_isGenericType;
			}
		}

        /// <summary>
        /// Gets a value indicating whether this instance is array item generic.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is array item generic; otherwise, <c>false</c>.
        /// </value>
        public bool IsArrayItemGenericType
        {
            get
            {
                return m_isArrayItemGenericType;
            }
        }

		/// <summary>
		/// Gets the generic sub types.
		/// </summary>
		/// <value>The generic sub types.</value>
		public ReadOnlyCollection<TypeName> GenericParameters
		{
			get
			{
				return Array.AsReadOnly<TypeName>( m_genericParameterTypeNames );
			}
		}

		/// <summary>
		/// Gets a value indicating whether this instance is array.
		/// </summary>
		/// <value><c>true</c> if this instance is array; otherwise, <c>false</c>.</value>
		public bool IsArray
		{
			get
			{
				return m_isArray;
			}
		}

		/// <summary>
		/// Gets the name of the display.
		/// </summary>
		/// <value>The name of the display.</value>
		public string DisplayName
		{
			get
			{
				return m_displayName;
			}
		}

		/// <summary>
		/// Gets the name of the base type.
		/// </summary>
		/// <value>The name of the base type.</value>
		public string BaseTypeName
		{
			get
			{
				return m_baseTypeName;
			}
		}

		/// <summary>
		/// Gets the name of the type.
		/// </summary>
		/// <value>The name of the type.</value>
		public string FullTypeName
		{
			get
			{
				return m_fullTypeName;
			}
		}

		/// <summary>
		/// Gets the name of the assembly.
		/// </summary>
		/// <value>The name of the assembly.</value>
		public string AssemblyName
		{
			get
			{
				return m_assemblyName;
			}
		}

		/// <summary>
		/// Gets the version.
		/// </summary>
		/// <value>The version.</value>
		public Version Version
		{
			get
			{
				return m_version;
			}
		}

		/// <summary>
		/// Gets the culture.
		/// </summary>
		/// <value>The culture.</value>
		public CultureInfo Culture
		{
			get
			{
				return m_culture;
			}
		}

		/// <summary>
		/// Gets the public key token.
		/// </summary>
		/// <value>The public key token.</value>
		public string PublicKeyToken
		{
			get
			{
				return m_publicKey;
			}
		}

		/// <summary>
		/// Gets the name of the fully qualified type.
		/// </summary>
		/// <value>The name of the fully qualified type.</value>
		public string FullyQualifiedTypeName
		{
			get
			{
				return string.Format( "{0}, {1}", FullTypeName, FullyQualifiedAssemblyName );
			}
		}

		/// <summary>
		/// Gets the name of the fully qualified assembly.
		/// </summary>
		/// <value>The name of the fully qualified assembly.</value>
		public string FullyQualifiedAssemblyName
		{
			get
			{
				return m_fullAssemblyName;
			}
		}

		/// <summary>
		/// Gets the name of the fully qualified type.
		/// </summary>
		/// <param name="filter">The filter.</param>
		/// <returns></returns>
		public string GetFullyQualifiedTypeName( TypeNameFilter filter )
		{
			StringBuilder quilifiedName = new StringBuilder();

			quilifiedName.Append( m_fullTypeName ).Append( ", " ).Append( m_assemblyName );

			// Process the Version information
			if( ( filter & TypeNameFilter.ExcludeVersion ) > 0 )
			{
				// Don't add anything
			}
			else if( ( filter & TypeNameFilter.IncludeEmptyVersion ) > 0 )
			{
				quilifiedName.Append( ", Version=0.0.0.0" );
			}
			else
			{
				quilifiedName.Append( ", Version=" ).Append( m_version.ToString() );
			}

			// Process the culture information
			if( ( filter & TypeNameFilter.ExcludeCulture ) > 0 )
			{
				// Don't add anything
			}
			else if( ( filter & TypeNameFilter.IncludeNeutralCulture ) > 0 )
			{
				quilifiedName.Append( ", Culture=neutral" );
			}
			else if( ( filter & TypeNameFilter.IncludeInvariantCulture ) > 0 )
			{
				quilifiedName.Append( ", Culture=" ).Append( InvariantCulture.Name );
			}
			else
			{
				quilifiedName.Append( ", Culture=" ).Append( m_culture.Name );
			}


			// Process the public key information
			if( ( filter & TypeNameFilter.ExcludePublicKey ) > 0 )
			{
				// Don't add anything
			}
			else 
			{
				quilifiedName.Append( ", PublicKeyToken=" ).Append( m_publicKey );
			}

			return quilifiedName.ToString();
		}

		/// <summary>
		/// Replaces the generic parameters.
		/// </summary>
		/// <param name="newGenericParameters">The new generic parameters.</param>
		/// <returns></returns>
		public TypeName ReplaceGenericParameters( TypeName[] newGenericParameters )
		{
			// Check Parameters
			ParamCode.Assert( m_isGenericType || m_isArrayItemGenericType, "This type is not a generic type." );
			ParamCode.AssertNotNull( newGenericParameters, "newGenericParameters" );
			ParamCode.Assert( newGenericParameters.Length == m_genericParameterTypeNames.Length, "newGenericParameters has differt number of elements than the generic parameters." );


			int pos = m_fullTypeName.IndexOf( GenericParameterStart );
			string typeName = m_fullTypeName.Substring( 0, pos + 1 );

			StringBuilder newTypeString = new StringBuilder();
			newTypeString.Append( typeName ).Append( newGenericParameters.Length ).Append( GenericParameterLeader );

			foreach( TypeName newName in newGenericParameters )
			{
				newTypeString.Append( GenericParameterLeader ).Append( newName.FullTypeName )
					.Append( TypeAssemblySeperator ).Append( ' ' ).Append( newName.FullyQualifiedAssemblyName )
					.Append( GenericParameterTrailer ).Append( GenericParameterSeperator );
			}

			// Remove the last seperator
			newTypeString.Remove( newTypeString.Length - 1, 1 );
			newTypeString.Append( GenericParameterTrailer );

            // Add ending [] for an Array with generic items
            if( m_fullTypeName.EndsWith( string.Format("{0}{1}", ArrayLeader, ArrayTrailer ) ))
            {
                newTypeString.AppendFormat( "{0}{1}", ArrayLeader, ArrayTrailer );
            }

			return new TypeName( m_fullAssemblyName, newTypeString.ToString() );
		}

		/// <summary>
		/// Finishes the setup.
		/// </summary>
		private void FinishSetup()
		{
			// Note: Microsoft does not consider an array a generic type even it it is an array of generices.
			if( m_genericParameterTypeNames != null && m_genericParameterTypeNames.Length > 0 )
			{
                if( m_isArray )
                {
                    m_isArrayItemGenericType = true;
                }
                else
                {
                    m_isGenericType = true;
                }
			}

			if( !string.IsNullOrEmpty( m_fullAssemblyName ) )
			{
				m_assemblyName = ParseFullAssemblyName( m_fullAssemblyName, out m_version, out m_culture, out m_publicKey );
			}
			else
			{
				m_fullAssemblyName = string.Empty;
				m_assemblyName = string.Empty;
			}

			// Build up the display name;
			StringBuilder text = new StringBuilder();
			BuildDisplayName( text );
			m_displayName = text.ToString();
			object a = m_displayName;
		}

		/// <summary>
		/// Parses the name of the type.
		/// </summary>
		/// <param name="name">The name.</param>
		/// <param name="baseTypeName">Name of the base type.</param>
		/// <param name="typeName">Name of the type.</param>
		/// <param name="assemblyName">Name of the assembly.</param>
		/// <param name="isArray">if set to <c>true</c> [is array].</param>
		/// <param name="genericParameters">The generic parameters.</param>
		private void ParseQualifiedName( string name, out string baseTypeName, out string typeName, out string assemblyName, out bool isArray, out TypeName[] genericParameters )
		{
			// Check Parameter
			ParamCode.AssertNotEmpty( name, "name" );

			baseTypeName = null;
			typeName = null;
			assemblyName = null;
			isArray = false;
			genericParameters = null;

			string[] parameterTypeStrings = null;
			
			int count = name.Length;
			for( int x = 0; x < count; x++ )
			{
				char ch = name[ x ];
				switch( ch )
				{
					case TypeAssemblySeperator:
						typeName = name.Substring( 0, x ).Trim();
						assemblyName = name.Substring( x + 1 ).Trim();
						if( baseTypeName == null )
						{
							baseTypeName = typeName;
						}
						return;

					case GenericParameterStart:
						baseTypeName = name.Substring( 0, x ).Trim();
						int numParams = GetGenericNumberOfParameters( name, x + 1, out x );
						parameterTypeStrings = new string[ numParams ];
						GetGenericParameters( name, x, parameterTypeStrings, out x );
						genericParameters = new TypeName[ numParams ];
						for( int y = 0; y < numParams; y++ )
						{
							genericParameters[ y ] = new TypeName( parameterTypeStrings[ y ] );
						}
						break;

					case ArrayLeader:
						if( name[ x + 1 ] == ArrayTrailer )
						{
							m_isArray = true;
							x++;
						}
						else
						{
							ThrowError( x + 1 );
						}
						break;
				}
			}

			// If we get here then there was not assembly name and this was just a type.
			typeName = name;
			if( baseTypeName == null )
			{
				baseTypeName = typeName;
			}
		}

		/// <summary>
		/// Parses the full name of the assembly.
		/// </summary>
		/// <param name="fullAssemblyName">Full name of the assembly.</param>
		/// <param name="version">The version.</param>
		/// <param name="cultureInfo">The culture info.</param>
		/// <param name="publicKey">The public key.</param>
		/// <returns></returns>
		private string ParseFullAssemblyName( string fullAssemblyName, out Version version, out CultureInfo cultureInfo, out string publicKey )
		{
			// Check Parameters
			ParamCode.AssertNotEmpty( fullAssemblyName, "fullAssemblyName" );
			// version is out parameter
			// culture is out parameter
			// publicKey is out parameter

			version = EmptyVersion;
			cultureInfo = InvariantCulture;
			publicKey = null;

			string[] items = fullAssemblyName.Split( TypeAssemblySeperator );

			// First item is the assembly name
			string assemblyName = items[ 0 ].Trim();

			int count = items.Length;
			for( int x = 1; x < count; x++ )
			{
				string item = items[ x ].Trim();
				// Split the item into the name and value 
				string[] nameValue = item.Split( NameValueSeperator );
				if( nameValue.Length > 1 )
				{
					string name = nameValue[ 0 ].Trim();
					string value = nameValue[ 1 ].Trim();

					switch( name.ToLower() )
					{
						case "version":
							version = new Version( value );
							break;

						case "culture":
							if( value == "neutral" )
							{
								cultureInfo = InvariantCulture;
							}
							else
							{
								cultureInfo = new CultureInfo( value );
							}
							break;

						case "publickeytoken":
							publicKey = value;
							break;
					}
				}
			}

			return assemblyName;
		}

		/// <summary>
		/// Gets the generic number of parameters.
		/// </summary>
		/// <param name="name">The name.</param>
		/// <param name="startOffset">The start offset.</param>
		/// <param name="endOffset">The end offset.</param>
		/// <returns></returns>
		private int GetGenericNumberOfParameters( string name, int startOffset, out int endOffset )
		{
			// Check Parameter
			ParamCode.AssertNotEmpty( name, "name" );
			ParamCode.AssertRange( startOffset, 0, name.Length - 1, "startOffset" );
			// endOffset is out parameter

			endOffset = -1;

			int numParams = 0;
			char ch = ' ';
			int count = name.Length;

			for( int x = startOffset; x < count; x++ )
			{
				ch = name[ x ];
				if( ch == SubTypeSeperator || ch == GenericParameterLeader )
				{
					endOffset = x;
					numParams = int.Parse( name.Substring( startOffset, endOffset - startOffset ) );
					break;
				}
			}

			// Move the the generic parameter leader (if we are not already there.
			while( ch != GenericParameterLeader && endOffset < count )
			{
				endOffset++;
				ch = name[ endOffset ];
			}

			return numParams;
		}

		/// <summary>
		/// Gets the generic parameters.
		/// </summary>
		/// <param name="name">The name.</param>
		/// <param name="startOffset">The start offset.</param>
		/// <param name="parameterStrings">The parameter strings.</param>
		/// <param name="endOffset">The end offset.</param>
		public void GetGenericParameters( string name, int startOffset, string[] parameterStrings, out int endOffset )
		{
			// Check Parameters
			ParamCode.AssertNotEmpty( name, "name" );
			ParamCode.AssertRange( startOffset, 0, name.Length - 1, "startOffset" );
			ParamCode.AssertNotNull( parameterStrings, "parameterStrings" );
			ParamCode.AssertRange( parameterStrings.Length, 1, int.MaxValue, "parameterStrings" );
			// endOffset is out parameters

			endOffset = -1;

			// The first value should be the GenericParameterLeader char
			if( name[ startOffset ] != GenericParameterLeader )
			{
				ThrowError( startOffset );
			}

			// Get the bracked parameters list.
			string text = GetTextInBracket( name, startOffset, out endOffset );

			// Seperate the paraemters
			int count = parameterStrings.Length;
			int length = text.Length;
			int pos = 0;

			for( int x = 0; x < count; x++ )
			{
				int end = 0;
				parameterStrings[ x ] = GetTextInBracket( text, pos, out end );
				if( end + 1 < length )
				{
					// Shoud be the trailer
					if( text[ end + 1 ] != GenericParameterTrailer )
					{
						ThrowError( pos );
					}

					if( end + 2 < length )
					{
						// If the next value is a seperator then move passed it
						if( text[ end + 2 ] == GenericParameterSeperator )
						{
							pos = end + 3;

						}
					}
				}
				else
				{
					ThrowError( pos );
				}
			}
		}

		/// <summary>
		/// Gets the text in bracket.
		/// </summary>
		/// <param name="name">The name.</param>
		/// <param name="startOffset">The start offset.</param>
		/// <param name="endOffset">The end offset.</param>
		/// <returns></returns>
		private string GetTextInBracket( string name, int startOffset, out int endOffset )
		{
			// Check Parameters
			ParamCode.AssertNotEmpty( name, "name" );
			ParamCode.AssertRange( startOffset, 0, name.Length - 1, "startOffset" );
			// endOffset is out parameters

			endOffset = -1;

			int count = name.Length;
			int bracketCount = 0;

			// The first value should be the GenericParameterLeader char
			if( name[ startOffset ] != GenericParameterLeader )
			{
				ThrowError( startOffset );
			}


			for( int x = startOffset; x < count; x++ )
			{
				switch( name[ x ] )
				{
					case GenericParameterLeader:
						bracketCount++;
						break;

					case GenericParameterTrailer:
						bracketCount--;
						if( bracketCount == 0 )
						{
							endOffset = x - 1;
							return name.Substring( startOffset + 1, endOffset - startOffset );
						}
						break;
				}
			}

			ThrowError( startOffset );
			return null;
		}

		/// <summary>
		/// Builds the name to have a pretty display version of the Type
		/// </summary>
		/// <param name="text">The text.</param>
		private void BuildDisplayName( StringBuilder text )
		{
			text.Append( BaseTypeName );
			if( IsGenericType )
			{
				text.Append( "< " );

				int x = 0;
				foreach( TypeName paramTypeName in GenericParameters )
				{
					if( x != 0 )
					{
						text.Append( ", " );
					}

					paramTypeName.BuildDisplayName( text );
					x++;
				}

				text.Append( " >" );
			}

			if( IsArray )
			{
				text.Append( "[]" );
			}
		}

		/// <summary>
		/// Throws the error.
		/// </summary>
		/// <param name="pos">The pos.</param>
		private void ThrowError( int pos )
		{
			throw new Exception( "Malformed type name, did not find generic parameter count position " + pos.ToString() );
		}
	}
}
